<?php

class Path
{
    public static $PathDelimiter = '/';

    public static function IsPathDelimiter($character)
    {
        return $character == Path::$PathDelimiter;
    }

    public static function IsAbsolutePath($path)
    {
        if (strlen($path) > 0)
            return Path::IsPathDelimiter($path[0]);
        else
            return false;
    }

    public static function IncludeTrailingPathDelimiter($path)
    {
        $result = $path;
        if (!Path::IsPathDelimiter($result[strlen($result) - 1]))
            $result .= Path::$PathDelimiter;
        return $result;
    }

    public static function Combine($prefix, $suffix)
    {
        if (Path::IsAbsolutePath($suffix) || !isset($prefix) || empty($prefix))
            return $suffix;
        else
            return Path::IncludeTrailingPathDelimiter($prefix) . $suffix;
    }

    public static function GetFileExtension($filePath)
    {
        return substr($filePath, strrpos($filePath, '.') + 1);
    }

    public static function GetFileTitle($filePath)
    {
        return pathinfo($filePath, PATHINFO_BASENAME);
    }
}

class JsPPPProcessor
{
    /** @var string */
    private $rootDir;

    /** @var string[] */
    private $processedFileNames;

    const ImportOnceDirective = 'import';

    private $recursionCount;

    /**
     * @param string $rootDir
     */
    public function __construct($rootDir, $importStrategy = null)
    {
        $this->rootDir = $rootDir;
        $this->processedFileNames = array();
        $this->recursionCount = 0;
    }

    private function GetFullFileName($fileName)
    {
        return Path::Combine($this->rootDir, $fileName);
    }

    /**
     * @param string $line
     * @return string
     */
    private function ExtractIncludeDirectiveFromLine($line)
    {
        $matches = array();
        $matchCount = preg_match('/(\/\/)\#>(\s*)'.self::ImportOnceDirective.'(\s*)\'(.*?)\'/', $line, $matches);
        if ($matchCount > 0)
            return $matches[4];
        else
            return null;
    }

    /**
     * @throws Exception
     * @param string $fileName
     * @param array|null $fileName
     * @return string
     */
    public function CompileFile($fileName, &$includedFiles = null)
    {
        if ($this->recursionCount == 0)
            $this->processedFileNames = array();

        $this->recursionCount++;

        $result = '';
        
        $fullFileName = $this->GetFullFileName($fileName);

        if (in_array($fullFileName, $this->processedFileNames))
        {
            $this->recursionCount--;
            return '';
        }
        
        if (!file_exists($fullFileName))
        {
            $this->recursionCount--;
            throw new Exception("File $fullFileName not found.");
        }

        $handle = fopen($fullFileName, "r");
        if ($handle)
        {
            while (($line = fgets($handle)) !== false)
            {
                $importOnceFileName = $this->ExtractIncludeDirectiveFromLine($line);
                if (!is_null($importOnceFileName))
                    $result .= $this->CompileFile($importOnceFileName, $includedFiles);
                else
                    $result .= $line . "\n";
            }
            if (!feof($handle))
            {
                $this->recursionCount--;
                throw new Exception("Error: unexpected fgets() fail. File name: $fileName.");
            }
            fclose($handle);
        }

        if (!is_null($includedFiles))
            array_push($includedFiles, $fileName);
        array_push($this->processedFileNames, $fullFileName);

        $this->recursionCount--;

        return $result;
    }
}

class JsPPPProcessorCacheDecorator 
{
    /** @var JsPPPProcessor */
    private $processor;

    private $rootDir;

    private $cacheDir;

    private $currentTime;

    /**
     * @param string $cacheDir
     */
    public function __construct($processor, $rootDir, $cacheDir, $currentTime)
    {
        $this->processor = $processor;
        $this->rootDir = $rootDir;
        $this->cacheDir = $cacheDir;
        $this->currentTime = $currentTime;
    }

    private function GetFullFileName($fileName)
    {
        return Path::Combine($this->rootDir, $fileName);
    }    

    private function InternalCompileFile($fileName, $cacheFileName, &$fileList)
    {
        $result = $this->processor->CompileFile($fileName, $fileList);

        $cachedFileList = '';
        foreach($fileList as $includedFile)
            $cachedFileList .= ($cachedFileList == '' ? '' : ',') . ('\'' . addslashes($includedFile) . '\'');

        file_put_contents($cacheFileName, 'return array(' . $cachedFileList . ');');

        return $result;
    }

    public function CompileFile($fileName, &$includedFiles = null)
    {
        $fileList = is_null($includedFiles) ? array() : $includedFiles;
        $cacheFileName = Path::Combine($this->cacheDir, md5($fileName) . '.php');


        if (!file_exists($cacheFileName))
        {
            $compiledContent = $this->InternalCompileFile($fileName, $cacheFileName, $fileList);
            file_put_contents(Path::Combine($this->cacheDir, $fileName), $compiledContent);
            return $compiledContent;
        }
        else
        {
            $list = eval(file_get_contents($cacheFileName));
            foreach($list as $includedFile)
            {
                if (filemtime($this->GetFullFileName($includedFile)) > $this->currentTime)
                {
                    $compiledContent = $this->InternalCompileFile($fileName, $cacheFileName, $fileList);
                    file_put_contents(Path::Combine($this->cacheDir, $fileName), $compiledContent);
                    return $compiledContent;
                }
            }
            return file_get_contents(Path::Combine($this->cacheDir, $fileName));
        }
    }
}

?>